/**
 * Project trac. Copyright May 29, 2012.
 */
package org.mediahouse.trac.util;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.regex.Pattern;

/**
 * @author Tin
 * @version %I%, %G%
 * 
 */
public final class JSONResponseDataContainer {

    private Object d;

    /**
     * @param responseData
     */
    public JSONResponseDataContainer(final Object responseData) {
        this.d = responseData;
    }

    public Object getData() {
        return this.d;
    }

    public void setData(final Object responseData) {
        this.d = responseData;
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(final Object obj) {
        return super.equals(obj);
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        return super.hashCode();
    }

    public String serialize() {
        if (d == null) return "{}";
        else return "{\"d\":" + this.new JSONSerializer().parseJSON(d) + "}";
    }

    /**
     * {@inheritDoc}
     * 
     * @see java.util.AbstractMap#toString()
     */
    @Override
    public String toString() {
        return this.serialize();
    }

    /**
     * @author Tin
     * @version %I%, %G%
     * 
     */
    public class JSONSerializer {

        /**
         * @param object
         * @return
         */
        private String parseJSONObject(final Map<String, Object> object) {
            if (object == null) return "null";
            else {
                final StringBuilder json = new StringBuilder();
                json.append('{');
                final Iterator<Entry<String, Object>> it = object.entrySet().iterator();
                Entry<String, Object> entry;
                while (it.hasNext()) {
                    entry = it.next();
                    json.append(parseJSONString(entry.getKey())).append(':');
                    json.append(parseJSON(entry.getValue()));
                    if (it.hasNext()) json.append(',');
                }
                json.append('}');
                return json.toString();
            }
        }

        /**
         * @param array
         * @return
         */
        private String parseJSONArray(final List<Object> array) {
            if (array == null) return "null";
            else {
                final StringBuilder json = new StringBuilder();
                json.append('[');
                final Iterator<Object> it = array.iterator();
                Object element;
                while (it.hasNext()) {
                    element = it.next();
                    json.append(parseJSON(element));
                    if (it.hasNext()) json.append(',');
                }
                json.append(']');
                return json.toString();
            }
        }

        /**
         * @param value
         * @return
         */
        private String parseJSONArray(final Object[] value) {
            return parseJSONArray(Arrays.asList(value));
        }

        /**
         * @param value
         * @return
         */
        private String parseJSONBoolean(final Object value) {
            if (value instanceof Boolean) return String.valueOf(value);
            else {
                final String s = String.valueOf(value);
                if (isParseableBoolean(s)) {
                    return String.valueOf(Boolean.valueOf(s));
                } else return "null";
            }
        }

        /**
         * @param s
         * @return
         */
        private boolean isParseableBoolean(final String s) {
            return true;
        }

        /**
         * @param value
         * @return
         */
        private String parseJSONString(final Object value) {
            if (value == null) return "null";
            else return "\"" + escapeJSONString(String.valueOf(value)) + "\"";
        }

        /**
         * @param in
         * @return
         */
        private String escapeJSONString(final String in) {
            if (in == null) return "null";
            else return in.replace("\\", "\\\\").replace("\"", "\\\"");
        }

        /**
         * @param value
         * @return
         */
        private String parseJSONNumber(final Object value) {
            if (value instanceof Number) return String.valueOf(value);
            else {
                final String s = String.valueOf(value);
                if (isParseableNumber(s)) {
                    return String.valueOf(Double.valueOf(s));
                } else return "null";
            }
        }

        /**
         * @param value
         * @return
         */
        private boolean isParseableNumber(final String value) {
            final String digits = "(\\p{Digit}+)";
            final String exp = "[eE][+-]?" + digits;
            final String hexDigits = "(\\p{XDigit}+)";
            return Pattern.matches((MessageFormat.format("[\\x00-\\x20]*[+-]?"
                    + "(NaN|Infinity|((({0}(\\.)?({0}?)({1})?)|(\\.({0})({1})?)|(((0[xX]{2}(\\.)?)|(0[xX]{2}?(\\.){2}))[pP][+-]?{0}))[fFdD]?))"
                    + "[\\x00-\\x20]*", digits, exp, hexDigits)), value);
        }

        /**
         * @param value
         * @return
         */
        private String tryParseJSONValue(final Object value) {
            if (value == null) return "null";
            else if (value instanceof Boolean) return parseJSONBoolean(value);
            else if (value instanceof Number) return parseJSONNumber(value);
            else if (value instanceof CharSequence) return parseJSONString(value);
            else if (value instanceof Character) return parseJSONString(value);
            else if (value instanceof Object) return parseJSONString(value);
            else throw new IllegalArgumentException("Cannot parse the given Java data structure into application/json.");
        }

        /**
         * @param value
         * @return
         */
        @SuppressWarnings("unchecked")
        public String parseJSON(final Object value) {
            if (value == null) return "null";
            else if (value instanceof Collection<?>) return parseJSONArray((List<Object>) value);
            else if (value.getClass().isArray()) return parseJSONArray((Object[]) value);
            else if (value instanceof Map<?, ?>) {
                return parseJSONObject((Map<String, Object>) value);
            } else return tryParseJSONValue(value);
        }
    }

    /**
     * @param args
     */
    public static void main(final String[] args) {
        final JSONResponseDataContainer.JSONSerializer json = new JSONResponseDataContainer(null).new JSONSerializer();

        // String tests:
        System.out.println();
        debug.trace(">>", "TESTING STRINGS...", "<<");
        debug.trace("String [null]:", json.parseJSONString(null));
        // Testing boolean input...
        debug.trace("String [boolean primitive]:", json.parseJSONString(true));
        debug.trace("String [boolean object]:", json.parseJSONString(new Boolean("tRUE")));
        // Testing number input...
        debug.trace("Number [integer primitive]:", json.parseJSONString(1));
        debug.trace("Number [integer hex primitive]:", json.parseJSONString(0x0123));
        debug.trace("Number [integer object]:", json.parseJSONString(new Integer("0")));
        debug.trace("Number [float]:", json.parseJSONString(new Random().nextFloat()));
        debug.trace("Number [double]:", json.parseJSONString(new Random().nextGaussian()));
        debug.trace("Number [not a number]:", json.parseJSONString(new String("NaN")));
        // Testing string/char input...
        debug.trace("String [simple string]:", json.parseJSONString("catinahat"));
        debug.trace("String [string with whitespace]:", json.parseJSONString("cat\tin\na hat\r"));
        debug.trace("String [alphanumeric string]:", json.parseJSONString("asd123"));
        debug.trace("String [alphanumeric digit first]:", json.parseJSONString("123asd"));
        debug.trace("String [numeric integer string]:", json.parseJSONString("765"));
        debug.trace("String [numeric float string]:", json.parseJSONString("1.0123"));
        debug.trace("String [empty string]:", json.parseJSONString(""));
        debug.trace("String [char]:", json.parseJSONString('3'));
        debug.trace("String [special character char]:", json.parseJSONString('{'));
        debug.trace("String [string with unescaped quote]:", json.parseJSONString("hello \"world"));
        debug.trace("String [string with unescaped backslash and quote]:", json.parseJSONString("hello \\\"world"));
        debug.trace("String [string with multiple unescaped backslashes and quote]:", json.parseJSONString("hello \\\\\"world"));
        debug.trace("String [string with newline control character]:", json.parseJSONString("hello\nworld"));
        debug.trace("String [empty string as object]:", json.parseJSONString(new String()));
        // Testing Java struct input...
        debug.trace(json.parseJSONString(new Object()));
        debug.trace(json.parseJSONString(new HashMap<String, Object>()));
        debug.trace(json.parseJSONString(new ArrayList<Object>()));
        debug.trace(json.parseJSONString(new Object[] { 1, true, "" }));
        debug.trace(json.parseJSONString(Arrays.asList(new Object[] { 1, "test" })));
        debug.trace(json.parseJSONString(new Object[] { 1, 2,
                new Object[] { 1, 2, 3 } }));

        // Number tests:
        System.out.println();
        debug.trace(">>", "TESTING NUMBERS...", "<<");
        debug.trace("Number [null]:", json.parseJSONNumber(null));
        // Testing boolean input...
        debug.trace("Number [boolean primitive]:", json.parseJSONNumber(true));
        debug.trace("Number [boolean object]:", json.parseJSONNumber(new Boolean("tRUE")));
        // Testing number input...
        debug.trace("Number [integer primitive]:", json.parseJSONNumber(1));
        debug.trace("Number [integer hex primitive]:", json.parseJSONNumber(0x0123));
        debug.trace("Number [integer object]:", json.parseJSONNumber(new Integer("0")));
        debug.trace("Number [float]:", json.parseJSONNumber(new Random().nextFloat()));
        debug.trace("Number [double]:", json.parseJSONNumber(new Random().nextGaussian()));
        debug.trace("Number [not a number]:", json.parseJSONNumber(new String("NaN")));
        // Testing string/char input...
        debug.trace("Number [simple string]:", json.parseJSONNumber("catinahat"));
        debug.trace("Number [string with whitespace]:", json.parseJSONNumber("cat\tin\na hat\r"));
        debug.trace("Number [alphanumeric string]:", json.parseJSONNumber("asd123"));
        debug.trace("Number [alphanumeric digit first]:", json.parseJSONNumber("123asd"));
        debug.trace("Number [numeric integer string]:", json.parseJSONNumber("765"));
        debug.trace("Number [numeric float string]:", json.parseJSONNumber("1.0123"));
        debug.trace("Number [empty string]:", json.parseJSONNumber(""));
        debug.trace("Number [char]:", json.parseJSONNumber('3'));
        debug.trace("Number [special character char]:", json.parseJSONNumber('{'));
        debug.trace("Number [string with unsecaped quote]:", json.parseJSONNumber("hello \"world"));
        debug.trace("Number [string with unescaped backslash and quote]:", json.parseJSONNumber("hello \\\"world"));
        debug.trace("Number [string with multiple unescaped backslashes]:", json.parseJSONNumber("hello \\\\\"world"));
        debug.trace("Number [string with newline control character]:", json.parseJSONNumber("hello\nworld"));
        debug.trace("Number [empty string as object]:", json.parseJSONNumber(new String()));
        // Testing Java struct input...
        debug.trace(json.parseJSONNumber(new Object()));
        debug.trace(json.parseJSONNumber(new HashMap<String, Object>()));
        debug.trace(json.parseJSONNumber(new ArrayList<Object>()));
        debug.trace(json.parseJSONNumber(new Object[] { 1, true, "" }));
        debug.trace(json.parseJSONNumber(Arrays.asList(new Object[] { 1, "test" })));
        debug.trace(json.parseJSONNumber(new Object[] { 1, 2,
                new Object[] { 1, 2, 3 } }));
    }
}